package puzzle.common.view.swing;

import java.awt.Component;
import java.io.IOException;
import java.io.Writer;
import java.util.Date;
import java.util.logging.Level;
import java.util.logging.Logger;

import puzzle.common.data.AbstractBoard;
import puzzle.common.solver.AbstractSolver;
import puzzle.common.solver.SolutionState;
import puzzle.common.solver.SolverAbortingException;
import puzzle.common.solver.SolverEvent;
import puzzle.common.solver.SolverEventListener;

public class PuzzleSolverController<B extends AbstractBoard<B>> extends Component {
	private static final long serialVersionUID = 1L;
	private static Logger cs_logger = Logger.getLogger(PuzzleSolverController.class.getName());

	public static String IS_SOLVER_THREAD_ACTIVE = "isSolverThreadActive";

	private PuzzleSolverAppMetadata<B> m_metadata;
	private AbstractPuzzleViewPanel<B> m_puzzleViewPanel;
	private long m_sleepTime = 10;
	private int m_detailLevel = 1;
	private SolverThread m_solverThread = null;
	private boolean m_isSolverThreadActive = false;
	private Writer m_statusWriter = null;
	
	private boolean m_isSuspensionRequested = false;
	private Object m_suspendLock = new Object();

	public PuzzleSolverController(PuzzleSolverAppMetadata<B> metadata) {
		m_metadata = metadata;
	}

	private synchronized SolverThread getSolverThread() {
		return m_solverThread;
	}

	public boolean isSolverThreadActive() {
		return m_isSolverThreadActive;
	}

	public void interrupt() {
		Thread solverThread = getSolverThread();

		if (solverThread != null) {
			solverThread.interrupt();
			try {
				solverThread.join();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}

	public synchronized boolean startSolver(B b) {
		if (isSolverThreadActive()) {
			printStatus("Another solver process is still running.");
			return false;
		}

		m_solverThread = new SolverThread(b);
		m_solverThread.start();
		m_isSuspensionRequested = false;
		m_isSolverThreadActive = true;
		firePropertyChange(IS_SOLVER_THREAD_ACTIVE, false, true);
		return true;
	}

	private void printStatus(String status) {
		try {
			if (m_statusWriter != null) {
				m_statusWriter.write(status);
			}
			System.out.println(status);
		} catch (IOException ex) {
			System.out.println(status);
		}
	}

	private class SolverThread extends Thread {
		private B m_board;

		public SolverThread(B b) {
			super("Solver Thread");
			setDaemon(true);
			m_board = b;
		}

		@Override
		public void run() {
			try {
				AbstractSolver<B, ?, ?> solver = m_metadata.createSolver();
				solver.setSolverEventListener(new SolverEventListener<SolverEvent<B>>() {
					public void onProgress(SolverEvent<B> e) throws SolverAbortingException {
						solver_onProgress(e);
					}
				});

				String resultString;
				Date startTimestamp = new Date();
				printStatus("START at " + startTimestamp);
				try {
					SolutionState result = solver.solveAll(m_board);
					resultString = result.toString();
				} catch (SolverAbortingException ex) {
					resultString = "ABORT";
				}
				Date endTimestamp = new Date();
				long elapsedTime = endTimestamp.getTime() - startTimestamp.getTime();
				printStatus(resultString + " at " + endTimestamp + " (" + elapsedTime / 1000.0
						+ " sec)");
				m_puzzleViewPanel.repaint();
			} finally {
				m_isSolverThreadActive = false;
				firePropertyChange(IS_SOLVER_THREAD_ACTIVE, true, false);
			}
		}

		private void solver_onProgress(SolverEvent<B> e) throws SolverAbortingException {
			if (cs_logger.isLoggable(Level.FINE)) {
				cs_logger.log(Level.FINE, e.toString());
			}
			
			if (Thread.interrupted()) {
				throw new SolverAbortingException();
			}
			
			try {
				handleSuspension();
			} catch (InterruptedException ex) {
				throw new SolverAbortingException();
			}

			if (!e.isChanged()) {
				return;
			}

			switch (e.getType()) {
			case THEORY_WHOLE:
				if (m_detailLevel != 1) return;
				break;
			case THEORY_STEP:
				if (m_detailLevel != 2) return;
				break;
			case THEORY_RULE:
				if (m_detailLevel != 3) return;
				break;
			}

			m_puzzleViewPanel.setBoard(e.getBoard());
			m_puzzleViewPanel.repaint();

			try {
				Thread.sleep(m_sleepTime);
			} catch (InterruptedException ex) {
				throw new SolverAbortingException();
			}
		}
	}

	public AbstractPuzzleViewPanel<B> getPuzzleViewPanel() {
		return m_puzzleViewPanel;
	}

	public void setPuzzleViewPanel(AbstractPuzzleViewPanel<B> puzzleViewPanel) {
		m_puzzleViewPanel = puzzleViewPanel;
	}

	public long getSleepTime() {
		return m_sleepTime;
	}

	public void setSleepTime(long sleepTime) {
		m_sleepTime = sleepTime;
	}

	public int getDetailLevel() {
		return m_detailLevel;
	}

	public void setDetailLevel(int detailLevel) {
		m_detailLevel = detailLevel;
	}

	public Writer getStatusWriter() {
		return m_statusWriter;
	}

	public void setStatusWriter(Writer statusWriter) {
		m_statusWriter = statusWriter;
	}
	
	public void suspend() {
		synchronized (m_suspendLock) {
			m_isSuspensionRequested = true;
		}
	}
	
	public void resume() {
		synchronized (m_suspendLock) {
			m_isSuspensionRequested = false;
			m_suspendLock.notify();
		}
	}
	
	public boolean isSuspensionRequested() {
		synchronized (m_suspendLock) {
			return m_isSuspensionRequested;
		}
	}
	
	private void handleSuspension() throws InterruptedException {
		synchronized (m_suspendLock) {
			while (m_isSuspensionRequested) {
				m_suspendLock.wait();
			}
		}
	}
}
